Предыдущая статья Оглавление
Следующая статья

Linux, Kernel, Firewall

Автор: ilya
ilya [ dog or doggy :) ] hackerdom.ru

  1. Введение
  2. Netfilter Hacking
  3. Firewall своими руками
  4. Пример norm.c
  5. Список литературы

Введение

Каждый уважающий себя администратор Linux должен уметь не только настраивать iptables, но и знать, как он работает. В этой статье речь пойдет не о том, как правильно настраивать iptables или какой-нибудь другой firewall, а о том, как работают firewall’ы в Linux.

В первую очередь, эта статья нацелена на читателей, которые занимаются (начинают или только хотят начать) программированием модулей ядра Linux (Linux Kernel Module – LKM), а также, надеюсь, поможет некоторым администраторам более детально разобраться в работе iptables. Все примеры в статье написаны для ядра 2.6.12.


Netfilter Hacking

Что такое netfilter и какое отношение он имеет к firewall’ам? В основном, Netfilter представляет собой набор функций (hook) расположенных в ядре, при помощи которых firewall’ы могут получать доступ к пакетам и, основываясь на правилах, решать, как с ними поступать дальше. Netfilter содержит 5 основных hook-функций, которые описаны в linux/netfilter_ipv4.h. Графически их можно изобразить так:

[INPUT]--->[1]--->[ROUTE]--->[3]---->[4]--->[OUTPUT]

		     |	              ^
		     |		      |
		     |		   [ROUTE]
		     v		      |
		    [2]		     [5]
		     |		      ^
		     |		      |
		     v		      |
		  [INPUT*]	   [OUTPUT*]

Описание:

[1] NF_IP_PRE_ROUTING – наша функция срабатывает, как только мы получаем пакет, даже если он проходящий. Если мы хотим иметь доступ ко всем пакетам, проходящим через наш интерфейс, то мы должны использовать эту функцию.
[2] NF_IP_LOCAL_IN – срабатывает в случае, когда пакет адресован нам, перед поступлением его в сетевой стек.
[3] NF_IP_FORWARD – если пакет необходимо смаршрутизировать с одного интерфейса на другой.
[4] NF_IP_POST_ROUTING – для исходящих пакетов из нашего сетевого стека.
[5] NF_IP_LOCAL_OUT – для всех исходящих пакетов.

После вызова функции и проведения нехитрых проверок над пакетом, нам нужно вынести вердикт, что делать с этим пакетом дальше. В нашем распоряжении 5 вариантов:

[1] NF_ACCEPT – пропускает пакет дальше.
[2] NF_DROP – отбрасывает пакет.
[3] NF_REPEAT – повторный вызов функции.
[4] NF_STOLEN – забирает пакет (прекращается передвижение).
[5] NF_QUEUE – ставит пакет в очередь, как правило, для передачи в пользовательское пространство (мы ведь работаем в пространстве ядра).

Вот собственно и все, что нужно для нормальной работы любого firewall’а в Linux. С одной стороны, набор функций, позволяющий получать доступ к пакетам практически в любой точке сетевого стека, а с другой, набор решений, как поступить с пакетом дальше.

/*
 * Я думаю что администраторам дальше можно не читать, там пойдет объяснение структур, правильность
 * их заполнения, а также примеры использования. Вся теория работы firewall’а заканчивается.
 */

Теперь попытаемся разобраться, как все это работает! Первым делом нам нужно познакомиться со структурой nf_hook_ops, она и будет нашим проводником в мир netfilter’a. Описание её можно найти в /Linux/netfilter.h:


44 struct nf_hook_ops
45 {
46         struct list_head list;
47 
48         /* User fills in from here down. */
49         nf_hookfn *hook;
50         int pf;
51         int hooknum;
52         /* Hooks are ordered in ascending priority. */
53         int priority;
54 };

Первое что мы видим, это “struct list_head list” – это структура, которая содержит список всех hook-функций, но нас она не сильно интересует.
nf_hookfn *hook – указатель на нашу функцию, в которой мы будем проводить все наши проверки. Возвращаемое значение должно быть одно из 5-и поведений (NF_ACCEPT, NF_DROP, ...).
int pf – служит для определения протокола, с которым мы хотим работать (PF_INET)
int hooknum – а вот и место нашего вызова. (например NF_IP_PRE_ROUTING)
int priority – приоритет. В случае если определено несколько функций на один вызов, первым сработает тот, у кого выше приоритет. Мы будем использовать – NF_IP_PRI_FIRST.

Не поверите, но это все! Остается лишь маленькое дополнение. После того как мы объявим и заполним нашу структуру, её необходимо зарегистрировать. Для этого служат 2-е функции, которые объявлены все в том же /Linux/netfilter.h:

89 /* Function to register/unregister hook points. */
90 int nf_register_hook(struct nf_hook_ops *reg);
91 void nf_unregister_hook(struct nf_hook_ops *reg);

Первая из них - это nf_register_hook, служит для регистрации нашей hook-функции. А nf_unregister_hook – для удаления нашей функции из цепочки.

Ничего особенного, просто банальное предупреждение. Обязательно выгружайте ваши функции при выгрузке модуля из памяти при помощи nf_unregister_hook. Если этого не сделать, произойдет очень неприятная вещь. Придет пакет, сработает наш вызов, ядро попытается обратиться к странице памяти для вызова нашей функции для обработки, а там…. эээ в лучшем случае ничего, в худшем ..кто-то занял наше место и тогда результат буде непредсказуем.


Firewall своими руками

Для примера напишем маленький firewall. Который будет беспощадно уничтожать все входящие и исходящие пакеты.


bash$ > cat firewall.c

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/netfilter_ipv4.h>
#include <linux/netfilter.h>


MODULE_LICENSE ("GPL v2");
MODULE_AUTHOR ("ill");
MODULE_DESCRIPTION ("Firewall");
 
/* 
* Объявление структур. Мы объявим 2-е структуры.
* 1-я для входящих пакетов
* 2-я для исходящих пакетов
*/
 
struct nf_hook_ops nf_incoming;
struct nf_hook_ops nf_outgoing;
int i=1;
 
unsigned int main_hook (unsigned int hooknum, 
            struct sk_buff **skb,
            const struct net_device *in,
            const struct net_device *out,
            int (*okfn)(struct sk_buff *))
{
 
/* Для примера, мы будем отбрасывать все пакеты */
    
return NF_DROP;
}
 
int init_module (void)
{
 
/*
* Заполнение структур
* Сначала, заполним структуру для входящих пакетов
*/
  
nf_incoming.hook=main_hook;
nf_incoming.pf=PF_INET;
nf_incoming.hooknum=NF_IP_PRE_ROUTING;
nf_incoming.priority=NF_IP_PRI_FIRST;
 
/* Теперь для исходящих */
 
nf_outgoing.hook=main_hook;
nf_outgoing.pf=PF_INET;
nf_outgoing.hooknum=NF_IP_POST_ROUTING;
nf_outgoing.priority=NF_IP_PRI_FIRST;
 
/* Вроде все, осталось только зарегистрировать наши функции */
 
nf_register_hook(&nf_incoming);
nf_register_hook(&nf_outgoing);
 
printk ("FireWall loaded\n");
 
return 0;
}
 
void cleanup_module (void)
{
 
/* Не забываем удалить наши вызовы :), а то конфуз может случиться */
 
nf_unregister_hook(&nf_incoming);
nf_unregister_hook(&nf_outgoing);
 
printk ("FireWall unload\n");
    
}

Ну вот. Все очень просто! Теперь компилируем наш модуль, для этого я пользуюсь вот таким Makefile'ом: (предположим, что исходный код сохраним с именем firewall.c, а исходники ядра находится в папке /usr/src/linux).


obj-m += firewall.o

all:
    make -C /usr/src/linux/ M=${PWD} modules
clean:
    make -C /usr/src/linux/ M=${PWD} clean

И запускаем: insmod firewall.ko (иногда приходится запускать с ключом -f: insmod -f firewall.ko, а то ему версии не нравятся, но кому не лень, можно в модуле прописать все данные о версии ядра :). Посмотрите /var/log/messages – если увидите "FireWall loaded", значит наш модуль загрузился. Теперь, если вы попробуете подключиться к кому-нибудь, или наоборот, кто-то захочет к вам подключиться, ничего не выйдет. Наш модуль не пропустит ни одного пакета. Чтобы вернуть все на место, просто выгрузите модуль командой rmmod firewall.

Вот пример firewall’a в 60 строк, включая заголовки. Не сложно, правда..!!! :) Теперь перейдем к более сложным вещам. Но совсем на чуть-чуть.


Пример norm.c

В этом примере мы будем проводить небольшой анализ захваченного нами пакета. Наша программа будет анализировать заголовки пакета и в случае неудовлетворения правилам будет удалять его или править. Итак, для начала, небольшое введение в структуру sk_buff:

sk_buff – это буфер для работы с пакетами. Как только приходит пакет или появляется необходимость его отправить, создается sk_buff куда и помещается пакет, а также сопутствующая информация, откуда, куда, для чего… На протяжении всего путешествия пакета в сетевом стеке используется sk_buff. Как только пакет отправлен или данные переданы пользователю, структура уничтожается, тем самым освобождая память. Описание этой структуры можно найти в linux/skbuff.h. Она очень большая, я не буду выкладывать её сюда :) Все, что мы будем использовать из неё, это:

Protocol – чтобы знать, с каким протоколом сетевого уровня мы имеем дело.
Data – место, где лежит пакет.

Более подробно о работе sk_buff можно почитать в Интернете, информации о нем море, а что касается практической части, советую почитать статью "Building Into The Linux Network Layer" из phrack №55

Ну вроде все, с теорией маленько разобрались. Теперь определимся, что и как мы будем делать. Так как это лишь пример использования, я не буду заострять внимание над нормализацией конкретного протокола, просто пробежимся немного по протоколам и все:

  1. IP– проверка протокола следующего уровня (пропускать только TCP, UDP, ICMP).
  2. IP – если поле TTL < 10 увеличиваем до 100 (не забывайте пересчитывать контрольную сумму :) после
    изменения содержимого пакета).
  3. ICMP – пропускаем только следующие пары:
    	Type=0 Code=0 – ответ (echo_reply) на ping 
    	Type=3 Code=0–15 -назначение недоступно (сеть, хост, порт…)
    	Type=8 Code=0 – запрос (echo) на ping
    	
  4. TCP – заблокируем порт 31337!

bash$ > cat norm.c 


#include <linux/module.h> /* Эйй, мы ведь пишем модуль  к ядру */
#include <linux/kernel.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
 
/*подключаем заголовки для работы с сетевыми протоколами */
/* и собственно говоря sk_buff */
 
#include <linux/skbuff.h>
#include <linux/inet.h>
#include <linux/ip.h>
#include <net/ip.h>
#include <linux/tcp.h>
#include <linux/icmp.h>
#include <asm/uaccess.h>
 
/* Увековечим свое имя Aha-ha… */
 
MODULE_LICENSE ("GPL v2");
MODULE_AUTHOR ("ill");
MODULE_DESCRIPTION ("Firewall");
 
struct nf_hook_ops nf_incoming;
struct sk_buff *skbf;
struct tcphdr *th;
struct icmphdr *icmph;
struct iphdr *iph;
 
unsigned int main_hook (unsigned int hooknum, 
            struct sk_buff **skb,
            const struct net_device *in, 
            const struct net_device *out,
            int (*okfn)(struct sk_buff*))
{
skbf=*skb;
 
/* Работаем только с IP */
if (skbf->protocol != htons(ETH_P_IP))
        return NF_ACCEPT;
 
/* Пропускаем только ICMP, TCP & UDP */
 
if (skbf->nh.iph->protocol != IPPROTO_TCP
    &&  skbf->nh.iph->protocol != IPPROTO_ICMP 
    && skbf->nh.iph->protocol != IPPROTO_UDP)
    return NF_DROP;
 
 
/* Проверка поля ttl */
 
if (ntohs(skbf->nh.iph->ttl)<10)
{
    skbf->nh.iph->ttl=htons(100);
    ip_send_check(skbf->nh.iph); /* подсчет checksum */
    return NF_ACCEPT;
    
}
 
/* Проверка ICMP, что мы здесь делаем, я думаю вы знаете :) */
 
if (skbf->nh.iph->protocol==IPPROTO_ICMP)
{
    skbf->h.icmph=(struct icmphdr *)(skbf->data+(skbf->nh.iph->ihl*4));
    if (skbf->h.icmph->type==ICMP_ECHOREPLY && skbf->h.icmph->code==0)
        return NF_ACCEPT;
 
    if (skbf->h.icmph->type==ICMP_DEST_UNREACH)
        return NF_ACCEPT;
 
    if (skbf->h.icmph->type==ICMP_ECHO && skbf->h.icmph->code==0)
        return NF_ACCEPT;
 
    
return NF_DROP;
}
 
/* Блокируем TCP, если порт источника или назначения 31337, и при этом делаем запись */
/* в messages */
 
if (skbf->nh.iph->protocol==IPPROTO_TCP)
{
    skbf->h.th=(struct tcphdr *)(skbf->data+(skbf->nh.iph->ihl*4));
    if (skbf->h.th->dest==htons(31337) || skbf->h.th->source==htons(31337))
    {
        printk ("Hacking attempt :) Good bye, young kiddies\n");
        return NF_DROP;
    }
 
return NF_ACCEPT;
}
 
 
/* Хех, если все прошло гладко, и никто не попал под наш мини-нормализатор */ 
/* милости просим в сетевой стек!!! */
 
return NF_ACCEPT;
}
 
 
int init_module ()
{
 
    nf_incoming.hook     = main_hook;
    nf_incoming.pf       = PF_INET;
    nf_incoming.hooknum  = NF_IP_PRE_ROUTING;
    nf_incoming.priority = NF_IP_PRI_FIRST;
 
 
    nf_register_hook(&nf_incoming);
 
    printk ("FireWall loaded\n");
    
    return 0;
}
 
void cleanup_module ()
{
nf_unregister_hook(&nf_incoming);
 
printk ("FireWall unload\n");
    
}

Ну вот и все, ребята. Теперь вы знаете, как работают firewall’ы в Linux и даже сможете написать свой собственный, знаете для чего служит netfilter и познакомились немного с работой сетевого стека ядра. Как видите, ничего сложного. Пару строк кода, и вы получаете доступ к святая святых, вы можете вертеть протоколами как угодно, менять их заголовки, отслеживать неприятеля и многое-многое другое…

P.S. Может кто подскажет, а как в Windows работает fireWall? :)

  1. Энциклопедия разработчика модулей ядра Linux (Linux Kernel Module Programming Guide)
  2. The Linux Kernel Module Programming Guide (на русском)
  3. Unreliable Guide To Hacking The Linux Kernel
  4. Hacking the Linux Kernel Network Stack
Предыдущая статья Оглавление
Следующая статья